/*
 * Copyright (c) 2009, David A. Freels Sr.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that
 * the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 *     * Neither the name of the <ORGANIZATION> nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package net.whippetcode.plugins.intellij.hudson.ui.job;

import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.vfs.VirtualFile;
import net.whippetcode.plugins.intellij.hudson.HudsonMonitorSettings;
import net.whippetcode.plugins.intellij.hudson.domain.job.Build;
import net.whippetcode.plugins.intellij.hudson.domain.job.Job;
import net.whippetcode.plugins.intellij.hudson.domain.job.Status;
import net.whippetcode.plugins.intellij.hudson.domain.job.Workspace;
import net.whippetcode.plugins.intellij.hudson.parser.ws.PopulateWorkspaceTask;
import net.whippetcode.plugins.intellij.hudson.ui.IViewListener;
import net.whippetcode.plugins.intellij.hudson.ui.StatusIndicator;
import net.whippetcode.plugins.intellij.hudson.ui.changeset.ChangeSetController;
import net.whippetcode.plugins.intellij.hudson.ui.test.HudsonTestController;
import net.whippetcode.plugins.intellij.hudson.util.IOUtil;
import net.whippetcode.plugins.intellij.hudson.util.IdeaUtil;
import net.whippetcode.plugins.intellij.hudson.util.LogUtil;
import net.whippetcode.util.task.*;

import javax.swing.*;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.tree.TreePath;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import java.util.logging.Level;

/**
 * This class implements the business logic for the Job control.
 *
 * @author David A. Freels Sr.
 */
public class HudsonJobController implements IViewListener, TreeExpansionListener, ICompletionCallbackHandler<ITask>
{
	private HudsonJobView view;
	private HudsonJobModel model;
	private StatusIndicator statusIndicator;
	private HudsonTestController testController;
	private ChangeSetController changeSetController;
	private HudsonMonitorSettings settings;
	private JTree jobTree;
	private final List<String> expandedPaths = new ArrayList<String>();
	private TaskManager taskManager;

	/**
	 * Create a new instance of the controller for the HudsonJob MVC.
	 *
	 * @param settings						Settings information.
	 * @param statusIndicator		 Control that displays the overall indicator.
	 * @param testController			The controller that handles the test display control.
	 * @param changeSetController The controller that handles the change set display control.
	 * @param taskManager				 The TaskManager used to execute asynchronous tasks.
	 */
	public HudsonJobController(HudsonMonitorSettings settings, StatusIndicator statusIndicator, HudsonTestController testController, ChangeSetController changeSetController, TaskManager taskManager)
	{
		this.statusIndicator = statusIndicator;
		this.testController = testController;
		this.changeSetController = changeSetController;
		this.settings = settings;
		this.taskManager = taskManager;
		jobTree = new JTree();
		jobTree.setCellRenderer(new JobCellRenderer());
		jobTree.setLargeModel(true);
		jobTree.addTreeExpansionListener(this);

		view = new HudsonJobView(jobTree, this);

		view.setViewEnabled(settings.isUseView());
		try
		{
			model = new HudsonJobModel(settings);
			jobTree.setModel(model.getJobTreeModel());
		}
		catch (MalformedURLException e)
		{
			LogUtil.log(Level.WARNING, e.getMessage(), e);
		}
	}

	public void update(HudsonMonitorSettings settings)
	{
		view.setViewEnabled(settings.isUseView());
		try
		{
			model.update(settings);
		}
		catch (MalformedURLException e)
		{
			LogUtil.log(Level.WARNING, e.getMessage(), e);
		}
	}

	/**
	 * Access to the view component.
	 *
	 * @return The view component.
	 */
	public HudsonJobView getView()
	{
		return view;
	}

	/**
	 * Refreshes the displayed Jobs.
	 */
	public void loadJobs()
	{
		taskManager.executeTask(new PopulateJobsTask(model.getServerURL(), this.settings.getServerAddress()), this);
	}

	private void updateJobs(PopulateJobsTask task)
	{
		try
		{
			statusIndicator.updateTooltip("Processing...");
			statusIndicator.updateStatus(Status.DISABLED);
			jobTree.removeTreeExpansionListener(this);
			model.loadJobs(task);
			model.getJobTreeModel().reload();
			restoreExpandedNodes();
			jobTree.addTreeExpansionListener(this);
			updateStatus();
		}
		catch (MalformedURLException e)
		{
			statusIndicator.updateTooltip("Invalid URL provided: " + model.getServerURL().toString());
			statusIndicator.updateStatus(Status.DISABLED);
			LogUtil.log(Level.WARNING, e.getMessage(), e);
		}
		catch (IOException e)
		{
			statusIndicator.updateTooltip("Could not connect to " + model.getServerURL().toString());
			statusIndicator.updateStatus(Status.DISABLED);
			LogUtil.log(Level.WARNING, e.getMessage(), e);
		}
		catch (Exception e)
		{
			LogUtil.log(Level.SEVERE, e.getMessage(), e);
		}
	}

	private void updateStatus()
	{
		statusIndicator.updateTooltip(model.getStatusSummary());
		statusIndicator.updateStatus(model.getStatus());
	}

	/**
	 * Attempts to expand all of the tree nodes that were expanded prior to the
	 * refresh.
	 */
	private void restoreExpandedNodes()
	{
		int rowCount;
		for (String path : expandedPaths)
		{
			rowCount = jobTree.getRowCount();
			for (int i = rowCount - 1; i > -1; i--)
			{
				if (path.equals(jobTree.getPathForRow(i).toString()))
				{
					jobTree.expandRow(i);
					break;
				}
			}
		}
	}

	public void handleEvent(EventObject event)
	{
		if (event instanceof ActionEvent)
		{
			String command = ((ActionEvent) event).getActionCommand();

			if (HudsonJobView.OPEN.equals(command) && model.getOpenURL() != null)
			{
				BrowserUtil.launchBrowser(model.getOpenURL().toString());
			}
			else if (HudsonJobView.RUN.equals(command) && model.getRunUrl() != null)
			{
				runJob();
			}
			else if (HudsonJobView.TESTS.equals(command) && model.getTestUrl() != null)
			{
				view.setTestEnabled(false);
				testController.loadTests(model.getTestUrl());
			}
			else if (HudsonJobView.VIEW.equals(command))
			{
				BrowserUtil.launchBrowser(settings.getServerAddress() + "/view/" + settings.getViewName());
			}
			else if(HudsonJobView.REFRESH.equals(command))
			{
				loadJobs();
			}
		}
		else if (event instanceof TreeSelectionEvent)
		{
			URL url = null;
			Object o = ((TreeSelectionEvent) event).getPath().getLastPathComponent();
			view.setRunEnabled(false);
			view.setTestEnabled(false);
			if (o instanceof Job)
			{
				url = ((Job) o).getJobURL();
				model.setRunUrl(url);
				view.setRunEnabled((url != null));
			}
			else if (o instanceof Build)
			{
				Build b = (Build) o;
				url = b.getURL();
				view.setTestEnabled((url != null));
				model.setTestUrl(url);
				changeSetController.loadChangeSet(b.getItems());
			}
			model.setOpenURL(url);
			view.setOpenEnabled((url != null));
		}
		else if (event instanceof WorkspaceEvent)
		{
			try
			{
				TreePath path = ((WorkspaceEvent) event).getPath();
				Workspace ws = (Workspace) path.getLastPathComponent();
				URL url = ws.getUrl();
				String baseDir = ws.getName();
				StringBuilder virtualPath = new StringBuilder(baseDir);
				while (!IdeaUtil.isBaseDirDirectory(baseDir))
				{
					path = path.getParentPath();
					ws = (Workspace) path.getLastPathComponent();
					baseDir = ws.getName();
					virtualPath.insert(0, baseDir + "/");
				}
				VirtualFile local = IdeaUtil.getFileByPath(virtualPath.toString());
				//TODO Send message if the file does not exist
				if (local != null)
				{
					File f = IOUtil.loadRemoteFile(url);
					IdeaUtil.compareFile(f, local);
				}
			}
			catch (Exception e)
			{
				LogUtil.log(Level.WARNING, "Error occurred while attempting file comparison.", e);
			}
		}
	}

	private void runJob()
	{
		if (settings.isRunActionOpensBrowser())
		{
			BrowserUtil.launchBrowser(model.getOpenURL().toString());
		}
		try
		{
			InputStream stream = IOUtil.getInputStream(model.getRunUrl());
			byte[] buffer = new byte[1024];
			int read;
			StringBuilder builder = new StringBuilder();
			while ((read = stream.read(buffer)) != -1)
			{
				builder.append(new String(buffer, 0, read));
			}

			LogUtil.log(Level.CONFIG, builder.toString());

			stream.close();
		}
		catch (IOException e)
		{
			LogUtil.log(Level.SEVERE, e.getMessage(), e);
		}
	}

	public void treeExpanded(TreeExpansionEvent treeExpansionEvent)
	{
		expandedPaths.add(treeExpansionEvent.getPath().toString());
		if (treeExpansionEvent.getPath().getLastPathComponent() instanceof Workspace)
		{
			TreePath treePath = treeExpansionEvent.getPath();
			Workspace ws = (Workspace) treePath.getLastPathComponent();
			Job job = (Job) treePath.getPathComponent(1);
			StringBuilder startPath = new StringBuilder(job.getName() + "/ws/");
			for (int i = 3; i < treePath.getPathCount(); i++)
			{
				startPath.append(((Workspace) treePath.getPathComponent(i)).getName()).append("/");
			}
			//Load all children of the exposed ws node.
			for (Workspace child : ws.getChildren())
			{
				if (child.isDirectory())
				{
					taskManager.executeTask(new PopulateWorkspaceTask(this.settings.getServerAddress(), startPath.toString() + child.getName(), child), this);
				}
			}
		}
	}

	public void treeCollapsed(TreeExpansionEvent treeExpansionEvent)
	{
		expandedPaths.remove(treeExpansionEvent.getPath().toString());
	}

	public void taskCompleted(CompletionStatus completionStatus, final ITask iTask, TaskExecutionException e)
	{
		if (iTask instanceof PopulateWorkspaceTask)
		{
			handleWorkspaceTaskCompletion(completionStatus);
		}
		else if (iTask instanceof PopulateJobsTask)
		{
			if (SwingUtilities.isEventDispatchThread())
			{
				this.updateJobs((PopulateJobsTask) iTask);
			}
			else
			{
				try
				{
					final HudsonJobController cont = this;
					SwingUtilities.invokeAndWait(new Runnable()
					{
						public void run()
						{
							cont.updateJobs((PopulateJobsTask) iTask);
						}
					});
				}
				catch (InterruptedException e1)
				{
					LogUtil.log(Level.SEVERE, e1.getMessage(), e1);
				}
				catch (InvocationTargetException e2)
				{
					LogUtil.log(Level.SEVERE, e2.getMessage(), e2);
				}
			}

		}
	}

	private void handleWorkspaceTaskCompletion(CompletionStatus completionStatus)
	{
		if (CompletionStatus.COMPLETED == completionStatus)
		{
			if (SwingUtilities.isEventDispatchThread())
			{
				jobTree.updateUI();
			}
			else
			{
				try
				{
					SwingUtilities.invokeAndWait(new Runnable()
					{
						public void run()
						{
							jobTree.updateUI();
						}
					});
				}
				catch (Exception e1)
				{
					LogUtil.log(Level.SEVERE, "Exception during workspace population.", e1);
				}

			}
		}
	}
}